Language TutorialGame Development with TSXOn this pageGame Development with TSX Welcome to the world of Dora SSR game development! If you're a frontend developer or familiar with TypeScript and React, you'll find using TSX to write games to be an exciting and familiar experience. If you're not, don't worry—this tutorial will guide you from scratch, showing how to develop games with Dora SSR and TSX while introducing some basic TSX concepts. 1. What is TSX? 1.1 TSX Basics TSX is a combination of TypeScript and JSX, allowing you to use HTML-like tag syntax within TypeScript to build interfaces and components. This is common in React development, and with Dora SSR, you can use TSX to define game objects and scenes. (Note: JSX is a JavaScript syntax extension that allows you to write XML-like code within JavaScript.) TipTo use TSX in Dora SSR, make sure to select TypeScript as the language and .tsx as the file extension when creating code files in the Web IDE. TSX Tags and Attributes: In TSX, you can use tags and attributes just like in HTML. For example: <sprite file="Image/logo.png" scaleX={0.2} scaleY={0.2}/> <sprite> is a tag representing a sprite (image) in the game. file, scaleX, and scaleY are attributes that set the sprite's file path and scale. TSX Function Components: You can create function components that accept properties and return TSX elements. This is similar to function components in React. interface ItemProps { x?: number; y?: number; children?: any;}const Item = (props: ItemProps) => { return ( <node x={props.x} y={props.y}> {/* Child elements */} {props.children} </node> );}; 2. Writing Game Scenes with TSX 2.1 Creating Simple Game Objects In Dora SSR, you can define game objects using TSX tags. For example, to create a sprite: <sprite file="Image/logo.png" scaleX={0.2} scaleY={0.2}/> 2.2 Converting to Renderable Object Instances To convert the TSX tag above into a renderable game object, you need to use the toNode() function. This function accepts a TSX element or array of elements and returns the corresponding game node. import { React, toNode } from 'DoraX';const node = toNode(<sprite file="Image/logo.png" scaleX={0.2} scaleY={0.2}/>); Now, node is an instantiated game scene node object. 3. Creating TSX Function Components Function components make your code more reusable and readable. Here's how to create a simple box component in Dora SSR. import { React, toNode } from 'DoraX';interface BoxProps { x?: number; y?: number; color?: number;}const Box = (props: BoxProps) => { return ( <draw-node x={props.x ?? 0} y={props.y ?? 0}> <rect-shape width={100} height={100} fillColor={props.color || 0xffffffff}/> </draw-node> );}; In the example above, Box is a function component that accepts x, y, and color properties and returns a draw node with a rectangle shape. Using the component: const boxes = [ <Box x={0} y={0} color={0xffff0000}/>, <Box x={150} y={0} color={0xff00ff00}/>, <Box x={300} y={0} color={0xff0000ff}/>,];const scene = toNode(boxes); In the example above, we create three boxes of different colors and place them in an array, which we then pass to the toNode() function to instantiate as game scene nodes. 4. Using useRef to Access Instantiated Objects In game development, you might need to directly manipulate a game object, such as changing its position, rotating it, or responding to events. The useRef() function helps you access the instantiated object for a TSX element. import { React, useRef, toNode } from 'DoraX';import { Body, BodyMoveType, Vec2 } from 'Dora';const boxRef = useRef<Body.Type>();const MovableBox = () => { return ( <body ref={boxRef} x={0} y={0} type={BodyMoveType.Dynamic}> <rect-fixture width={100} height={100}/> <draw-node> <rect-shape width={100} height={100} fillColor={0xffffffff}/> </draw-node> </body> );};const scene = toNode( <physics-world> <MovableBox/> </physics-world>);// Now, you can manipulate the instantiated object via boxRef.currentif (boxRef.current) { boxRef.current.position = Vec2(200, 200);} In the example above, boxRef is a reference to the instantiated object of the <body> tag. You can access and manipulate this object through boxRef.current. 5. Creating Class Components In addition to function components, you can also create class-based components. Class components can hold state and lifecycle methods, allowing more flexible management of component behavior. However, class components are relatively more complex, so function components are generally recommended. import { React, toNode, useRef } from 'DoraX';import { Label } from 'Dora';// Define initial properties for the Counter componentinterface CounterProps { count: number;}// Create a Counter component by extending React.Componentclass Counter extends React.Component<CounterProps> { count: number; labelRef: JSX.Ref<Label.Type>; // Constructor to accept initial properties constructor(props: CounterProps) { super(props); this.count = props.count; this.labelRef = useRef<Label.Type>(); } // Render function to return TSX elements render() { return ( <label ref={this.labelRef} text={this.count.toString()} fontName='sarasa-mono-sc-regular' fontSize={80} onTapped={this.onTapped}/> ); } // Tap event handler onTapped = () => { if (this.labelRef.current) { this.labelRef.current.text = (++this.count).toString(); } };}// Instantiate the Counter componenttoNode(<Counter count={1}/>); In the example above, we create a counter component Counter that holds a count value and a label. When the label is tapped, the count increases, and the label text is updated. 6. Complete Example: Creating a Simple Game Let's put everything together and create a simple game that includes: A movable character (sprite) Some static obstacles (boxes) Tap the screen to control the character's movement 6.1 Define the Player Component import { React, useRef, toNode } from 'DoraX';import { Body, BodyMoveType, Vec2 } from 'Dora';const playerRef = useRef<Body.Type>();const Player = () => { return ( <body ref={playerRef} x={0} y={0} type={BodyMoveType.Dynamic} linearAcceleration={Vec2.zero} linearDamping={1}> <rect-fixture width={50} height={50}/> <draw-node> <rect-shape width={50} height={50} fillColor={0xff00ff00}/> </draw-node> </body> );}; In the code above, Player is a player component with a movable rectangle body. playerRef is a reference that points to the instantiated player object. 6.2 Define the Obstacle Component const Obstacle = (props: {x: number; y: number}) => { return ( <body type={BodyMoveType.Static} x={props.x} y={props.y}> <rect-fixture width={100} height={100}/> <draw-node> <rect-shape width={100} height={100} fillColor={0xffff0000}/> </draw-node> </body> );}; Obstacle is an obstacle component that consists of a static rectangular body. You can set the position of the obstacle by passing x and y as properties. 6.3 Create the Game Scene const GameScene = () => { return ( <physics-world onTapBegan={touch => { // Control player movement to the tap location const {current: player} = playerRef; if (player) { player.velocity = touch.location .sub(player.position) .normalize() .mul(300); } }}> <Player/> <Obstacle x={200} y={0}/> <Obstacle x={-200} y={0}/> </physics-world> );}; In the GameScene component, we create a physics world, listen for tap events, and control the player's movement toward the tap location. We also add two obstacles. Note that all physics bodies must be child nodes inside the physics-world component. 6.4 Run the Game Instantiate the scene node: const scene = toNode(<GameScene/>); Now, you've created a simple game where the character can move on the screen and interact with obstacles. 7. Some Special TSX Elements In Dora SSR, there are some special TSX elements that provide functionality beyond creating scene nodes. 7.1 Action Elements Action elements are used to perform a series of actions, such as moving, rotating, scaling, etc. You can use action elements in TSX and add them as child elements to game objects. <sprite file="Image/logo.png"> <move time={0.5} startX={0} startY={0} stopX={200} stopY={200}/></sprite> In the example above, we create a sprite and add a move action to make it move from (0, 0) to (200, 200). This animation will automatically play when the parent node is created. <sprite file="Image/logo.png"> <loop> <move time={0.5} startX={0} startY={0} stopX={200} stopY={200}/> <move time={0.5} startX={200} startY={200} stopX={0} stopY={0}/> </loop></sprite> A looping action can be created using the <loop> tag, which allows the action sequence of child elements to loop continuously. If you want to create an action sequence that plays only once, you can use the <sequence> tag instead. For simultaneous playback of multiple actions, you can use the <spawn> tag in combination. Note that <loop>, <sequence>, and <spawn> tags can only contain action elements, and the <loop> tag can only be used as the outermost nested tag, while <sequence> and <spawn> tags can be combined freely. If you want to create an action sequence to be used later, you can wrap it with an <action> tag at the outermost layer. import { ActionDef, Sprite } from "Dora";import { React, toNode, useRef } from "DoraX";// Create a function component containing a sprite and action sequenceconst ActionNode = () => { // Create references const spriteRef = useRef<Sprite.Type>(); const actionRef = useRef<ActionDef.Type>(); // Play the action sequence in the click event handler const onTapped = () => { const {current: sprite} = spriteRef; const {current: action} = actionRef; if (sprite && action && sprite.actionCount === 0) { sprite.perform(action); } }; // Return the sprite and action sequence to be used later return ( <sprite ref={spriteRef} file="Image/logo.png" onTapped={onTapped}> <action ref={actionRef}> <sequence> <move time={0.5} startX={0} startY={0} stopX={200} stopY={200}/> <move time={0.5} startX={200} startY={200} stopX={0} stopY={0}/> </sequence> </action> </sprite> );};// Instantiate the componenttoNode(<ActionNode/>); In the example above, we create a component ActionNode containing a sprite and an action sequence, which plays the action sequence when tapped. Here’s the English version of your MDX content: 7.2 Currently Supported Action Elements Currently, Dora SSR supports the following action elements: <action>: Creates a referenceable action sequence. <anchor-x>: Continuously changes the X anchor point of the node. <anchor-y>: Continuously changes the Y anchor point of the node. <angle>: Continuously changes the angle of the node (Z-axis). <angle-x>: Continuously changes the rotation angle on the X-axis. <angle-y>: Continuously changes the rotation angle on the Y-axis. <delay>: Adds a delay in the animation timeline. <event>: Triggers an event. <width>: Continuously changes the width of the node. <height>: Continuously changes the height of the node. <hide>: Hides the node. <show>: Shows the node. <move>: Continuously changes the node's X and Y position. <move-x>: Continuously changes the node's X position. <move-y>: Continuously changes the node's Y position. <move-z>: Continuously changes the node's Z position. <opacity>: Continuously changes the opacity of the node. <roll>: Continuously changes the rotation of the node. <scale>: Continuously changes the scale of the node on both X and Y axes. <scale-x>: Continuously changes the scale of the node on the X-axis. <scale-y>: Continuously changes the scale of the node on the Y-axis. <skew-x>: Continuously changes the skew on the X-axis. <skew-y>: Continuously changes the skew on the Y-axis. <frame>: Creates a frame animation. <loop>: Repeats an action. <spawn>: Executes a group of actions in parallel. <sequence>: Executes a series of actions sequentially. 7.3 Descriptive Elements In addition to action elements, Dora SSR provides descriptive elements that further describe the appearance and behavior of the game objects to be created. <physics-world> <contact groupA={0} groupB={1} enabled={false}/> {/* Defines the physical group collision relationship */} <body type={BodyMoveType.Dynamic} group={0}> <disk-fixture radius={50}/> {/* Defines the collision shape of the physical body */} <draw-node> <dot-shape radius={50}/> {/* Defines the shape of the draw node */} </draw-node> </body> <effek-node> <effek file='Particle/effek/Laser01.efk'/> {/* Defines the information for playing effects */} </effek-node></physics-world> In the example above, we use several descriptive elements, including <contact>, <disk-fixture>, <dot-shape>, and <effek>. These elements are not instantiated as independent game objects but are used to complement the description of the physical properties, collision shapes, draw shapes, and effect information for the parent game objects to be created. 7.4 Currently Supported Descriptive Elements Currently, Dora SSR supports the following descriptive elements: Draw shape elements usable under <draw-node>: <dot-shape>: Draws a dot or a filled circle. <segment-shape>: Draws a line segment. <polygon-shape>: Draws a polygon. <rect-shape>: Draws a rectangle. <verts-shape>: Draws a polygon where each vertex has its own color. Collision shape elements usable under <body>: <rect-fixture>: Defines a rectangular collision body. <polygon-fixture>: Defines a polygonal collision body. <multi-fixture>: Defines a concave collision body composed of multiple convex polygons. <disk-fixture>: Defines a circular collision body. <chain-fixture>: Defines a chain-shaped collision body composed of line segments. 7.5 Using <custom-node> to Create Custom Nodes If you need to create a custom game node, you can use the <custom-node> element. This element allows you to reuse program modules written in code other than TSX to create other game objects. You can encapsulate this code as a new TSX component. // Import a button component written in non-TSX codeimport * as ButtonCreate from 'UI/Control/Basic/Button';import { Button } from 'UI/Control/Basic/Button';// Define properties for the new TSX button componentinterface ButtonProps { ref?: JSX.Ref<Button.Type>; text: string; width: number; height: number; onClick?: () => void;}// Create a new TSX button component using `<custom-node>`// and reuse the code of the imported external componentconst Button = (props: ButtonProps) => { return <custom-node onCreate={() => { const button = ButtonCreate({ text: props.text, width: props.width, height: props.height }); button.onTapped(() => { if (props.onClick) { props.onClick(); } }); if (props.ref) { (props.ref.current as any) = button; } return button; }}/>;};// Use the new TSX button componenttoNode( <Button text="Button" width={60} height={60}/>); In the example above, we create a new TSX button component using the <custom-node> element, reusing the code of an externally imported button component written in non-TSX code. This way, you can use custom game nodes in Dora SSR, extending the functionality of the TSX language to create game objects. 7.6 Using <custom-element> to Create Custom Elements If you need to create descriptive data using TSX syntax and implement the parsing, instantiation, or rendering of this descriptive data yourself, you can use the <custom-element> element. This element allows you to customize the creation and processing logic of game objects, enabling more flexible framework extensions. // Define a custom element Iteminterface ItemProps { value: number;}const Item = (props: ItemProps) => { return <custom-element name='Item' data={props}/>;};// Define a custom element Listinterface ListProps { children?: any | any[];}const List = (props: ListProps) => { return <custom-element name='List' data={props}/>;};// Create custom JSX descriptive dataconst jsxObject = ( <List> <Item value={0}/> <Item value={1}/> </List>);// Print the created JSX descriptive datap(jsxObject); The output of jsxObject is as follows: { [type] = "custom-element" [children] = { } [props] = { [name] = "List" [data] = { [children] = { [1] = { [type] = "custom-element" [children] = { } [props] = { [name] = "Item" [data] = { [value] = 0 [children] = { } } } } [2] = { [type] = "custom-element" [children] = { } [props] = { [name] = "Item" [data] = { [value] = 1 [children] = { } } } } } } }} In the example above, we create two custom elements Item and List using the <custom-element> element and create JSX descriptive data containing these two custom elements. You can continue to write programs to access this descriptive data object and implement custom object creation and processing logic. 8. Conclusion In this tutorial, we covered how to develop games using Dora SSR and TSX, including: TSX basics: tags, attributes, and function components Using toNode() to convert TSX tags into renderable object instances Using useRef to access instantiated game objects Creating custom game components Combining components to build a complete game scene Dora SSR provides a familiar yet powerful platform for frontend developers, allowing you to easily apply your existing TypeScript and TSX knowledge to game development. Now, try extending this example by adding more game elements and logic, and explore more of Dora SSR's features! Appendix: Key Functions and Types Here are some important functions and types used in Dora SSR: React.createElement: Used to create TSX elements. Typically, this doesn't need to be used directly as it's called automatically by the engine. toNode(enode): Converts a TSX element or array of elements into game nodes. useRef<T>(item?: T): Creates a reference to access instantiated objects. preloadAsync(enode, handler?): Asynchronously preloads the art assets required by a node. Using these functions will help you efficiently write game logic and interfaces in Dora SSR. I hope this tutorial has been helpful, and I wish you success in your game development journey!